﻿#include "mine_view.h"




enum{
    GAME_STARTED,
    GAME_FLAG,
    GAME_TIME,
    GAME_OVER,
    LAST_SIGNAL
};

static guint mine_view_signals[LAST_SIGNAL]= {0};

static void mine_view_init(MineView *obj);
static void mine_view_finalize(GObject *obj);
#if 0
static void mine_view_get_preferred_width (GtkWidget* base, gint* minimum, gint* natural);
static void mine_view_get_preferred_height (GtkWidget* base, gint* minimum, gint* natural);
#endif

static void start_clock(MineView *view);
static void stop_clock(MineView *view);
static void place_mines(MineView *view);
static void sweep_empty_point(MinePoint *mp);
static void sweep_adjacent_points(MinePoint *mp);
static void redraw_mine_point(MinePoint *mp);
static void explode(MinePoint *mp);
static gboolean is_complete(MineView *view);
static void sweep(MinePoint *mp, gboolean sweepAdjacent);
static gboolean mine_point_button_press_cb(GtkWidget *widget, GdkEventButton *event, MinePoint *mp);
static gboolean mine_point_button_release_cb(GtkWidget *widget, GdkEventButton *event, MinePoint *mp);

G_DEFINE_TYPE(MineView, mine_view, GTK_TYPE_GRID)

static void mine_view_class_init(MineViewClass *clazz)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(clazz);
    obj_class->finalize = mine_view_finalize;

#if 0    
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(clazz);
    widget_class->get_preferred_height = mine_view_get_preferred_height;
    widget_class->get_preferred_width = mine_view_get_preferred_width;
#endif

#if 0
    clazz->game_over = NULL;
#endif

    mine_view_signals[GAME_STARTED] = g_signal_new("game-started"
                                                , G_TYPE_FROM_CLASS(obj_class)
                                                , G_SIGNAL_RUN_FIRST
                                                , 0
                                                , NULL, NULL
                                                , NULL
                                                , G_TYPE_NONE
                                                , 0);

    mine_view_signals[GAME_FLAG] = g_signal_new("game-flag"
                                                , G_TYPE_FROM_CLASS(obj_class)
                                                , G_SIGNAL_RUN_FIRST
                                                , 0
                                                , NULL, NULL
                                                , NULL
                                                , G_TYPE_NONE
                                                , 1, G_TYPE_INT);

    mine_view_signals[GAME_TIME] = g_signal_new("game-time"
                                                , G_TYPE_FROM_CLASS(obj_class)
                                                , G_SIGNAL_RUN_FIRST
                                                , 0
                                                , NULL, NULL
                                                , NULL
                                                , G_TYPE_NONE
                                                , 1, G_TYPE_INT);


    mine_view_signals[GAME_OVER] = g_signal_new("game-over"
                                                , G_TYPE_FROM_CLASS(obj_class)
                                                , G_SIGNAL_RUN_FIRST
#if 0                                                
                                                , G_STRUCT_OFFSET(MineViewClass, game_over)
#else
                                                , 0
#endif                                                
                                                , NULL, NULL
                                                , NULL
                                                , G_TYPE_NONE
                                                , 1, G_TYPE_BOOLEAN);

}

static void mine_view_init(MineView *obj)
{
    gtk_grid_set_row_homogeneous((GtkGrid*)obj, TRUE);
    gtk_grid_set_column_homogeneous((GtkGrid*)obj, TRUE);
#if 0    
    gtk_grid_set_row_spacing((GtkGrid*)obj, 0);
    gtk_grid_set_column_spacing((GtkGrid*)obj, 0);
    gtk_widget_set_can_focus((GtkWidget*)obj, TRUE);
    //g_object_set(obj, "expand", TRUE, NULL);
#endif    
}


static void mine_view_finalize(GObject *obj)
{
    MineView *view = MINE_VIEW(obj);
    g_free(view->mine_points);

    stop_clock(view);

    G_OBJECT_CLASS(mine_view_parent_class)->finalize(obj);
}

static gboolean tick_cb(MineView *view)
{
    g_signal_emit(view, mine_view_signals[GAME_TIME], 0, (gint)g_timer_elapsed(view->timer, NULL));
    return TRUE;    //go on the timeout source
}

static void start_clock(MineView *view)
{
    if(view->timer)
        stop_clock(view);
    
    g_signal_emit(view, mine_view_signals[GAME_TIME], 0, 0);
    view->timer = g_timer_new();
    view->clock = g_timeout_add(500, (GSourceFunc)tick_cb, view);
}

static void stop_clock(MineView *view)
{
    if(view->timer)
    {
        g_timer_destroy(view->timer);
        view->timer = 0;
    }
    if(view->clock)
    {
        g_source_remove(view->clock);
        view->clock = 0;
    }
}

#if 0

static gint get_minnum_size(MineView *view)
{
    gint w = 320 / view->col_count;
    gint h = 200 / view->row_count;
    int s = MIN(w,h);
    if(s< 20)
        s = 20;
    return s;
}

static void mine_view_get_preferred_width (GtkWidget* base, gint* minimum, gint* natural)
{    
    g_assert(base);
    if(base)
    {
        MineView *view = MINE_VIEW(base);
        *minimum = *natural = view->col_count * get_minnum_size(view);
    }
    else
    {
        *minimum = *natural = 0;            
    }
    printf("width nat=%d\n", *natural);
}

static void mine_view_get_preferred_height (GtkWidget* base, gint* minimum, gint* natural)
{
    g_assert(base);
    if(base)
    {
        MineView *view = MINE_VIEW(base);
        *minimum = *natural = view->row_count * get_minnum_size(view);
    }
    else
    {
        *minimum = *natural = 0;            
    }

    printf("height nat=%d\n", *natural);
}
#endif


//create new mine points
static void place_mines(MineView *view)
{
    g_assert(view->state == Mine_State_0);
    view->state = Mine_State_Placed;

    gint row_count = view->row_count;
    gint col_count = view->col_count;

#define UseSpecificRand     1

#if UseSpecificRand
    GRand *rand= g_rand_new_with_seed(0);
#endif

    //place mine_count mines and avoid cluster
    gint i = 0;
    while(i < view->mine_count)
    {

#if UseSpecificRand
        gint x = g_rand_int_range(rand, 0, col_count);
        gint y = g_rand_int_range(rand, 0, row_count);
#else
        gint x = g_random_int_range(0, col_count);
        gint y = g_random_int_range(0, row_count);
#endif
        MinePoint *p = &view->mine_points[y* col_count + x];

        if(!p->has_mine)
        {
            //place mine
            p->has_mine = TRUE;
            i++;

            //calc the adj count
            for(gint rx = -1; rx <= 1; rx++)
            {
                if(rx + x < 0 || rx +x >= col_count)
                    continue;

                for(gint ry = -1; ry <= 1; ry++)
                {
                    if(ry + y < 0 || ry + y >= row_count)
                        continue;
                    if(ry == 0 && rx == 0)
                        continue;

                    MinePoint *adj = &view->mine_points[(ry+y)*col_count + (rx + x)];
                    adj->adjacent_count++;
                }
            }
        }
    }
#if UseSpecificRand
    g_rand_free(rand);
#endif

}

//挖出连片的空地
static void sweep_empty_point(MinePoint *mp)
{
    g_assert(!mp->swept && !mp->has_mine);

    if(mp->flag == Mine_Flag_Flag)
    {
#if 0        
        mp->flag = Mine_Flag_None;
        view->flag_count--;
        g_signal_emit(view, mine_view_signals[GAME_FLAG], 0, view->mine_count - view->flag_count);
#endif        
        return;
    }

    MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));
    mp->swept = TRUE;        
    redraw_mine_point(mp);
    view->swept_count++;



    if(mp->adjacent_count  == 0)
    {
    
        //calc the adj count
        for(gint rx = -1; rx <= 1; rx++)
        {
            if(rx + mp->x < 0 || rx + mp->x >= view->col_count)
                continue;

            for(gint ry = -1; ry <= 1; ry++)
            {
                if(ry + mp->y < 0 || ry + mp->y >= view->row_count)
                    continue;

                MinePoint *next = &view->mine_points[(ry + mp->y) * view->col_count + (rx + mp->x)];
                if(!next->swept)
                {

                    sweep_empty_point(next);
                }
            }
        }        
    }
}

//满足邻接数量时， 开出邻接的地雷。
static void sweep_adjacent_points(MinePoint *mp)
{
    g_assert(mp->swept);
    MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));

    //calc the sum of adjacent flags and cleared mines
    gint cleared_count = 0;
    for(gint rx = -1; rx <= 1; rx ++)
    {
        if(rx + mp->x < 0 || rx + mp->x >= view->col_count)
            continue;

        for(gint ry = -1; ry <= 1; ry++)
        {
            if(ry + mp->y < 0 || ry + mp->y >= view->row_count)
                continue;

            MinePoint *next = &view->mine_points[(ry + mp->y) * view->col_count + (rx + mp->x)];
            if((next->swept && next->has_mine) || next->flag == Mine_Flag_Flag)
            {
                cleared_count++;
            }
        }
    }

    if(cleared_count == mp->adjacent_count)
    {
        for(gint rx = -1; rx <= 1; rx++)
        {
            if(rx + mp->x < 0 || rx + mp->x >= view->col_count)
                continue;

            for(gint ry = -1; ry <= 1; ry++)
            {
                if(rx == 0 && ry == 0)                
                    continue;
                if(ry + mp->y < 0 || ry + mp->y >= view->row_count)
                    continue;

                MinePoint *next = &view->mine_points[(ry + mp->y) * view->col_count + (rx + mp->x)];
                if(!next->swept && next->flag != Mine_Flag_Flag)
                {
                    sweep(next, FALSE);
                }
            }
        }
    }
}

static void redraw_mine_point(MinePoint *mp)
{
    if(mp->swept)
    {
        if(mp->has_mine)
        {
            mine_point_add_class(mp, "exploded");
        }
        else
        {
            mine_point_remove_class(mp, "maybe");
            mine_point_remove_class(mp, "flag");
            mine_point_add_class(mp, "count");
            if(mp->adjacent_count > 0)
            {
                g_assert(mp->adjacent_count > 0 && mp->adjacent_count < 9);
                gchar *adj_count = g_strdup_printf("%umines", mp->adjacent_count);
                mine_point_add_class(mp, adj_count);
                g_free(adj_count);
            }
        }
    }
    else
    {
        MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));
        if(view->state == Mine_State_Exploded)
        {
            if(mp->flag == Mine_Flag_Flag)
            {
                if(!mp->has_mine)
                    mine_point_add_class(mp, "incorrect");
            }
            else
            {
                if(mp->has_mine)
                    mine_point_add_class(mp, "mine");
            }

        }
        else
        {
            if(mp->flag == Mine_Flag_Flag)
            {
                mine_point_add_class(mp, "flag");
            }
            else if(mp->flag == Mine_Flag_Maybe)
            {
                mine_point_add_class(mp, "maybe");
            }
            else
            {
                mine_point_remove_class(mp, "maybe");
                mine_point_remove_class(mp, "flag");
            }

        }
    }
}


static void explode(MinePoint *mp)
{    
    mp->swept = TRUE;

    MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));
    view->state = Mine_State_Exploded;

    //mark all mines and wrong flags
    for(gint y = 0; y < view->row_count; y++)
        for(gint x = 0; x < view->col_count; x++)
        {
            MinePoint *p = &view->mine_points[y * view->col_count + x];
            if(p->has_mine || p->flag == Mine_Flag_Flag)
            {
                redraw_mine_point(p);
            }
        }

    printf("emit game_over fail\n");
    
    g_signal_emit(view, mine_view_signals[GAME_OVER], 0, TRUE);       //TRUE is the param; return type is void, so ignore its arg
    stop_clock(view);
}

static gboolean is_complete(MineView *view)
{
    return view->swept_count == view->col_count * view->row_count - view->mine_count;
}

static void sweep(MinePoint *mp, gboolean sweepAdjacent)
{
    g_assert(mp->widget);
    g_assert(gtk_widget_get_parent(mp->widget));
    MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));

    if(mp->swept)
    {       
        //sweep adjacent mines
        if(sweepAdjacent)
        {
            sweep_adjacent_points(mp);
        }
    }
    else
    {       //sweep
        if(mp->flag == Mine_Flag_Flag)
            return;

        if(mp->has_mine)    //explode
        {
            explode(mp);
            return;
        }
        else
        {        
            sweep_empty_point(mp);
        }
    }

    //check whether complete
    if(is_complete(view))
    {
        view->state = Mine_State_Over;        

        //marked uncovered mines
        for(gint y = 0; y < view->row_count; y++)
            for(gint x = 0; x < view->col_count; x++)
            {
                MinePoint *p = &view->mine_points[y * view->col_count + x];              
                if(p->has_mine && !p->swept && p->flag != Mine_Flag_Flag)
                {
                    p->flag = Mine_Flag_Flag;
                    view->flag_count++;
                    redraw_mine_point(p);
                    g_signal_emit(view, mine_view_signals[GAME_FLAG], 0, view->mine_count - view->flag_count);                    
                }
            }

        printf("emit game_over ok\n");
        g_signal_emit(view, mine_view_signals[GAME_OVER], 0, FALSE);
        stop_clock(view);
    }
}

static gboolean mine_point_button_press_cb(GtkWidget *widget, GdkEventButton *event, MinePoint *mp)
{
    printf("button_press\n");
    if(event->button == GDK_BUTTON_PRIMARY)
    {

    }
    else if(event->button == GDK_BUTTON_SECONDARY)
    {
        //switch flag mark
        MineView *view = (MineView*)gtk_widget_get_parent((GtkWidget*)mp->widget);
        if(!mp->swept)
        {
            if(mp->flag == Mine_Flag_None)
            {
                mp->flag = Mine_Flag_Flag;
                view->flag_count++;
            }
            else if(mp->flag == Mine_Flag_Flag)
            {
                mp->flag = view->use_question_flag ? Mine_Flag_Maybe : Mine_Flag_None;
                view->flag_count--;
            }
            else    
                mp->flag = Mine_Flag_None;

            redraw_mine_point(mp);
            g_signal_emit(view, mine_view_signals[GAME_FLAG], 0, view->mine_count - view->flag_count);
        }
    }

    return FALSE;   //propagate further
}


static gboolean mine_point_button_release_cb(GtkWidget *widget, GdkEventButton *event, MinePoint *mp)
{
    if(event->button != GDK_BUTTON_PRIMARY)
        return FALSE;

    printf("button_release\n");    
    MineView *view = MINE_VIEW(gtk_widget_get_parent(mp->widget));
    if(view->state == Mine_State_Placed)
    {
        view->state = Mine_State_Started;
        g_signal_emit(view, mine_view_signals[GAME_STARTED], 0);
        start_clock(view);
    }
    
    if(view->state == Mine_State_Started)
    {
        sweep(mp, TRUE);
    }

    return FALSE;   //propagate further
}

GtkWidget* mine_view_new(void)
{
    return g_object_new(TYPE_MINE_VIEW, NULL); 
}

void mine_view_force_start_game(MineView *view, gint row_count, gint col_count, gint mine_count)
{
    view->state = Mine_State_0;

    mine_view_set_size(view, row_count, col_count, mine_count);
    view->swept_count = 0;
    view->flag_count = 0;

    place_mines(view);
    
    g_signal_emit(view, mine_view_signals[GAME_FLAG], 0, view->mine_count - view->flag_count);
    g_signal_emit(view, mine_view_signals[GAME_TIME], 0, 0);
}

void mine_view_start_game(MineView *view, gint row_count, gint col_count, gint mine_count)
{    
    if(view->state == Mine_State_Started)    
    {
        GtkDialog *dialog = (GtkDialog*) gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel((GtkWidget*)view)),
                                                                    GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
                                                                    GTK_MESSAGE_QUESTION,
                                                                    GTK_BUTTONS_YES_NO,
                                                                    "Discard current game, and start new?");
        int res = gtk_dialog_run(dialog);
        gtk_widget_destroy((GtkWidget*)dialog);

        if(res != GTK_RESPONSE_YES)
            return;
    }

    mine_view_force_start_game(view, row_count, col_count, mine_count);
}

gboolean mine_view_set_size(MineView *view, gint row_count, gint col_count, gint mine_count)
{
    gboolean res = FALSE;
     
    g_assert(view->state == Mine_State_0);

    //if(view->row_count != row_count || view->col_count != col_count || view->mine_count != mine_count)
    {
        res = TRUE;

        //destroy old mine points and widgets
        gtk_container_foreach((GtkContainer*)view, (GtkCallback)gtk_widget_destroy, NULL);
        g_free(view->mine_points);

        //create new mine points and widgets
        view->mine_points = g_new0(MinePoint, row_count*col_count+1);

        GtkWidget *widget;
        GtkGrid *grid = GTK_GRID(view);
        for(gint y = 0; y< row_count; y++)
            for(gint x = 0; x< col_count; x++)
            {
                widget = gtk_button_new();

                MinePoint *mp = &view->mine_points[y*col_count + x];
                mp->x = x;
                mp->y = y;
                mp->widget = widget;                
                mine_point_add_class(mp, "tile");
#if 0
                GtkStyleContext *sc = gtk_widget_get_style_context(widget);
                gtk_style_context_add_class(sc, "tile");
#endif
                g_signal_connect(widget, "button-press-event", (GCallback)mine_point_button_press_cb, mp);
                g_signal_connect(widget, "button-release-event", (GCallback)mine_point_button_release_cb, mp);
                gtk_widget_show(widget);                
                gtk_grid_attach(grid, widget, x, y, 1, 1);

                g_assert(mp->widget);
                g_assert(gtk_widget_get_parent(mp->widget));

            }

        view->row_count = row_count;
        view->col_count = col_count;
        view->mine_count = mine_count;
    }

    return res;
}

void mine_view_get_size(MineView *view, gint *row_count, gint *col_count, gint *mine_count)
{
    *row_count = view->row_count;
    *col_count = view->col_count;
    *mine_count = view->mine_count;
}




void mine_point_add_class(MinePoint *mp, const gchar* style_class)
{
    GtkStyleContext *sc = gtk_widget_get_style_context(mp->widget);
    gtk_style_context_add_class(sc, style_class);
}

void mine_point_remove_class(MinePoint *mp, const gchar* style_class)
{
    GtkStyleContext *sc = gtk_widget_get_style_context(mp->widget);
    gtk_style_context_remove_class(sc, style_class);
}

